package com.emeraldparser.symbol;

import java.util.ListIterator;

import com.emeraldparser.util.Automaton;
import com.emeraldparser.util.Automatons;
import com.emeraldparser.util.HashCodes;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;

/**
 * A symbol class that represents a sequence of zero or more symbol classes concatenated together.
 * 
 * @author Adam Paynter
 *
 */
public final class ConcatenationSymbolClass extends AbstractSymbolClass {

	private final ImmutableList<? extends SymbolClass> concatenatedSymbolClasses;
	private final transient int hashCode;

	public ConcatenationSymbolClass(SymbolClass... concatenatedSymbolClasses) {
		this(ImmutableList.copyOf(concatenatedSymbolClasses));
	}

	public ConcatenationSymbolClass(Iterable<? extends SymbolClass> concatenatedSymbolClasses) {
		this(ImmutableList.copyOf(concatenatedSymbolClasses));
	}

	public ConcatenationSymbolClass(ImmutableList<? extends SymbolClass> concatenatedSymbolClasses) {
		this.concatenatedSymbolClasses = Preconditions.checkNotNull(concatenatedSymbolClasses, "Concatenated symbol classes must not be null");
		this.hashCode = HashCodes.classSpecificHashCode(ConcatenationSymbolClass.class, concatenatedSymbolClasses);
	}

	public ImmutableList<? extends SymbolClass> getConcatenatedSymbolClasses() {
		return concatenatedSymbolClasses;
	}

	@Override
	public Automaton<? extends State, ? extends SymbolClass> toAutomaton() {
		Automaton<State, SymbolClass> automaton = Automatons.create(super.toAutomaton());
		
		IndexedState currentState = new IndexedState(0);
		automaton.addVertex(currentState);
		automaton.addStartVertex(currentState);
		
		ListIterator<? extends SymbolClass> iterator = concatenatedSymbolClasses.listIterator();
		while (iterator.hasNext()) {
			SymbolClass symbolClass = iterator.next();
			
			IndexedState nextState = new IndexedState(iterator.nextIndex());
			automaton.addVertex(nextState);
			
			automaton.addDirectedEdge(currentState, nextState, symbolClass);
			
			currentState = nextState;
		}
		
		automaton.addAcceptVertex(currentState);
		
		return automaton;
	}

	@Override
	public String toString() {
		return "(" + Joiner.on(" ").join(concatenatedSymbolClasses) + ")";
	}

	@Override
	public int hashCode() {
		return hashCode;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		
		if (obj == null) {
			return false;
		}
		
		if (getClass() != obj.getClass()) {
			return false;
		}
		
		ConcatenationSymbolClass other = (ConcatenationSymbolClass) obj;
		
		return Objects.equal(concatenatedSymbolClasses, other.concatenatedSymbolClasses);
	}

	private class IndexedState implements SymbolClass.State {
		
		private final int index;
		private final transient int hashCode;
		
		IndexedState(int index) {
			this.index = index;
			this.hashCode = Objects.hashCode(ConcatenationSymbolClass.this, index);
		}
		
		@Override
		public String toString() {
			StringBuilder text = new StringBuilder();
			
			text.append("(");
			
			String delimiter = "";
			ListIterator<? extends SymbolClass> iterator = concatenatedSymbolClasses.listIterator();
			while (iterator.hasNext()) {
				int index = iterator.nextIndex();
				SymbolClass symbolClass = iterator.next();
				
				if (index == this.index) {
					text.append(".");
				} else {
					text.append(delimiter);
				}
				
				text.append(symbolClass);
				
				delimiter = " ";
			}
			
			if (index == concatenatedSymbolClasses.size()) {
				text.append(".");
			}
			
			text.append(")");
			
			return text.toString();
		}

		@Override
		public int hashCode() {
			return hashCode;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj) {
				return true;
			}
			
			if (obj == null) {
				return false;
			}
			
			if (getClass() != obj.getClass()) {
				return false;
			}
			
			IndexedState other = (IndexedState) obj;
			
			return Objects.equal(getConcatenationSymbolClass(), other.getConcatenationSymbolClass())
			    && index == other.index;
		}

		private ConcatenationSymbolClass getConcatenationSymbolClass() {
			return ConcatenationSymbolClass.this;
		}

	}

	@Override
	public <I, O> O accept(SymbolClassVisitor<I, O> visitor, I input) {
		return visitor.visitConcatenationSymbolClass(this, input);
	}

}
